luci-app-example: rewrite RPC side using ucode
authorGeorge Sapkin <[email protected]>
Mon, 3 Feb 2025 13:55:55 +0000 (15:55 +0200)
committerPaul Donald <[email protected]>
Mon, 3 Feb 2025 14:07:18 +0000 (15:07 +0100)
Signed-off-by: George Sapkin <[email protected]>
applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js
applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js
applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js
applications/luci-app-example/root/etc/uci-defaults/80_example
applications/luci-app-example/root/usr/libexec/rpcd/luci.example [deleted file]
applications/luci-app-example/root/usr/share/rpcd/ucode/example.uc [new file with mode: 0755]
applications/luci-app-example/structure.md

index eb0c965e6980a3b7cbef3a1a260ec83bb507e802..69fe8c8a2a3b19d6fd3e343269190033cbcf4b58 100644 (file)
@@ -9,8 +9,7 @@ listed by the shell command
 
 $ ubus list
 
-Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file
-in that directory will be the value for the object key in the declared map.
+Custom ucode scripts can be placed in /usr/share/rpcd/ucode, and must emit JSON.
 
 Permissions to make these calls must be granted in /usr/share/rpcd/acl.d
 via a file named the same as the application package name (luci-app-example)
index 26ab5eee841e637a027db1ee16eccf40b1d1b85a..a0731408f03e04dd843e1b535de9df4b9b4f72e8 100644 (file)
@@ -9,8 +9,7 @@ listed by the shell command
 
 $ ubus list
 
-Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file
-in that directory will be the value for the object key in the declared map.
+Custom ucode scripts can be placed in /usr/share/rpcd/ucode, and must emit JSON.
 
 Permissions to make these calls must be granted in /usr/share/rpcd/acl.d
 via a file named the same as the application package name (luci-app-example)
index d04e1e66dbbedb52ddf2e90ad263c45bce7f8335..3b8f82129c48eb8c0c6ce05381f5f170d8c51cae 100644 (file)
@@ -9,8 +9,7 @@ listed by the shell command
 
 $ ubus list
 
-Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file
-in that directory will be the value for the object key in the declared map.
+Custom ucode scripts can be placed in /usr/share/rpcd/ucode, and must emit JSON.
 
 Permissions to make these calls must be granted in /usr/share/rpcd/acl.d
 via a file named the same as the application package name (luci-app-example)
@@ -104,9 +103,7 @@ return view.extend({
        // return is used to modify the DOM that the browser shows.
        render: function (data) {
                // data[0] will be the result from load_sample1
-               var sample1 = data[0] || {};
-               // data[1] will be the result from load_sample_yaml
-               var sample_yaml = data[1] || {};
+               const sample1 = data[0] || {};
 
                // Render the tables as individual sections.
                return E('div', {}, [
index f89694316950ff74fddeb8100bc492744b855297..aad59eb845c53cf30cad9d3d3c2a86c543ebed9c 100644 (file)
@@ -4,6 +4,10 @@ touch /etc/config/example
 uci set example.first=first
 uci set example.second=second
 uci set example.third=third
-uci commit
+uci set example.animals=animals
+uci set example.animals.num_cats=1
+uci set example.animals.num_dogs=2
+uci set example.animals.num_parakeets=4
+uci commit example
 
 return 0
diff --git a/applications/luci-app-example/root/usr/libexec/rpcd/luci.example b/applications/luci-app-example/root/usr/libexec/rpcd/luci.example
deleted file mode 100755 (executable)
index e3e5f87..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/usr/bin/env lua
-
--- If you need filesystem access, use nixio.fs
-local fs = require "nixio.fs"
-
--- LuCI JSON is used for checking the arguments and converting tables to JSON.
-local jsonc = require "luci.jsonc"
-
--- Nixio provides syslog functionality
-local nixio = require "nixio"
-
--- To access /etc/config files, use the uci module
-local UCI = require "luci.model.uci"
-
--- Slight overkill, but leaving room to do log_info etcetera.
-local function log_to_syslog(level, message) nixio.syslog(level, message) end
-
-local function log_error(message)
-    log_to_syslog("err", "[luci.example]: " .. message)
-end
-
-local function using_uci_directly(section)
-    -- Rather than parse files in /etc/config, you can rely on the
-    -- luci.model.uci module.
-    local uci = UCI.cursor()
-
-    -- https://openwrt.github.io/luci/api/modules/luci.model.uci.html
-    local config_name = uci:get("example", section)
-
-    uci.unload("example")
-
-    if not config_name then
-        local msg = "'" .. section .. "' not found in /etc/config/example"
-        -- Send the log message to syslog so it can be found with logread
-        log_error(msg)
-
-        -- Convert a lua table into JSON notation and print to stdout
-        -- .stringify() is equivalent to cjson's .encode()
-        print(jsonc.stringify({uci_error = msg}))
-
-        -- Indicate failure in the return code
-        os.exit(1)
-    end
-
-    return config_name
-end
-
--- The methods table defines all of the APIs to expose to rpcd.
--- rpcd will execute this Lua file with the 'list' argument to discover the
--- method names that can be presented over ubus, as well as any arguments
--- those methods take.
-local methods = {
-    -- How to call this API:
-    -- echo '{"section": "first"}' | lua /usr/libexec/rpcd/luci.example call get_uci_value
-    -- echo '{"section": "does_not_exist"}' | lua /usr/libexec/rpcd/luci.example call get_uci_value
-    get_uci_value = {
-        -- Args are specified as a table, where the argument type is specified by example
-        -- The value is not used as a default.
-        args = {section = "a_string"},
-        -- A special key of 'call' points to a function definition for execution.
-        call = function(args)
-            -- A table for the result.
-            local r = {}
-            r.result = jsonc.stringify({
-                example_section = using_uci_directly(args.section)
-            })
-            -- The 'call' handler will refer to '.code', but also defaults if not found.
-            r.code = 0
-            -- Return the table object; the call handler will access the attributes
-            -- of the table.
-            return r
-        end
-    },
-    -- How to call this API:
-    -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample1
-    -- ubus call luci.example get_sample1
-    get_sample1 = {
-        call = function()
-            local r = {}
-            -- This structure does not map well to a JSONMap in the LuCI form setup.
-            -- It can be rendered as a table easily enough with loops.
-            r.result = jsonc.stringify({
-                num_cats = 1,
-                num_dogs = 2,
-                num_parakeets = 4,
-                is_this_real = false,
-                not_found = nil
-            })
-            return r
-        end
-    },
-    -- How to call this API:
-    -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample2
-    -- ubus call luci.example get_sample2
-    get_sample2 = {
-        call = function()
-            local r = {}
-            -- This is the structural data that JSONMap will work with in the JS file
-            local data = {
-                option_one = {
-                    name = "Some string value",
-                    value = "A value string",
-                    parakeets = {"one", "two", "three"},
-                },
-                option_two = {
-                    name = "Another string value",
-                    value = "And another value",
-                    parakeets = {3, 4, 5},
-                }
-            }
-            r.result = jsonc.stringify(data)
-            return r
-        end
-    }
-}
-
-local function parseInput()
-    -- Input parsing - the RPC daemon calls the Lua script and
-    -- sends input to it via stdin, not as an argument on the CLI.
-    -- Thus, any testing via the lua interpreter needs to be in the form
-    -- echo '{jsondata}' | lua /usr/libexec/rpcd/script call method_name
-    local parse = jsonc.new()
-    local done, err
-
-    while true do
-        local chunk = io.read(4096)
-        if not chunk then
-            break
-        elseif not done and not err then
-            done, err = parse:parse(chunk)
-        end
-    end
-
-    if not done then
-        print(jsonc.stringify({
-            error = err or "Incomplete input for argument parsing"
-        }))
-        os.exit(1)
-    end
-
-    return parse:get()
-end
-
-local function validateArgs(func, uargs)
-    -- Validates that arguments picked out by parseInput actually match
-    -- up to the arguments expected by the function being called.
-    local method = methods[func]
-    if not method then
-        print(jsonc.stringify({error = "Method not found in methods table"}))
-        os.exit(1)
-    end
-
-    -- Lua has no length operator for tables, so iterate to get the count
-    -- of the keys.
-    local n = 0
-    for _, _ in pairs(uargs) do n = n + 1 end
-
-    -- If the method defines an args table (so empty tables are not allowed),
-    -- and there were no args, then give a useful error message about that.
-    if method.args and n == 0 then
-        print(jsonc.stringify({
-            error = "Received empty arguments for " .. func ..
-                " but it requires " .. jsonc.stringify(method.args)
-        }))
-        os.exit(1)
-    end
-
-    uargs.ubus_rpc_session = nil
-
-    local margs = method.args or {}
-    for k, v in pairs(uargs) do
-        if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then
-            print(jsonc.stringify({
-                error = "Invalid argument '" .. k .. "' for " .. func ..
-                    " it requires " .. jsonc.stringify(method.args)
-            }))
-            os.exit(1)
-        end
-    end
-
-    return method
-end
-
-if arg[1] == "list" then
-    -- When rpcd starts up, it executes all scripts in /usr/libexec/rpcd
-    -- passing 'list' as the first argument. This block of code examines
-    -- all of the entries in the methods table, and looks for an attribute
-    -- called 'args' to see if there are arguments for the method.
-    --
-    -- The end result is a JSON struct like
-    -- {
-    --   "api_name": {},
-    --   "api2_name": {"host": "some_string"}
-    -- }
-    --
-    -- Which will be converted by ubus to 
-    --  "api_name":{}
-    --  "api2_name":{"host":"String"}
-    local _, rv = nil, {}
-    for _, method in pairs(methods) do rv[_] = method.args or {} end
-    print((jsonc.stringify(rv):gsub(":%[%]", ":{}")))
-elseif arg[1] == "call" then
-    -- rpcd will execute the Lua script with a first argument of 'call',
-    -- a second argument of the method name, and a third argument that's
-    -- stringified JSON.
-    --
-    -- To debug your script, it's probably easiest to start with direct
-    -- execution, as calling via ubus will hide execution errors. For example:
-    -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample2
-    --
-    -- or
-    --
-    -- echo '{"section": "firstf"}' | /usr/libexec/rpcd/luci.example call get_uci_value
-    --
-    -- See https://openwrt.org/docs/techref/ubus for more details on using
-    -- ubus to call your RPC script (which is what LuCI will be doing).
-    local args = parseInput()
-    local method = validateArgs(arg[2], args)
-    local run = method.call(args)
-    -- Use the result from the table which we know to be JSON already.
-    -- Anything printed on stdout is sent via rpcd to the caller. Use
-    -- the syslog functions, or logging to a file, if you need debug
-    -- logs.
-    print(run.result)
-    -- And exit with the code supplied.
-    os.exit(run.code or 0)
-elseif arg[1] == "help" then
-    local helptext = [[
-Usage:
-
- To see what methods are exported by this script:
-
-    lua luci.example list
-
- To call a method that has no arguments:
-
-    echo '{}' | lua luci.example call method_name
-
- To call a method that takes arguments:
-
-    echo '{"valid": "json", "argument": "value"}' | lua luci.example call method_name
-
- To call this script via ubus:
-
-    ubus call luci.example method_name '{"valid": "json", "argument": "value"}'
-]]
-    print(helptext)
-end
diff --git a/applications/luci-app-example/root/usr/share/rpcd/ucode/example.uc b/applications/luci-app-example/root/usr/share/rpcd/ucode/example.uc
new file mode 100755 (executable)
index 0000000..d6e21fc
--- /dev/null
@@ -0,0 +1,48 @@
+#!/usr/bin/env ucode
+
+'use strict';
+
+import { cursor } from 'uci';
+
+// Rather than parse files in /etc/config, we can use `cursor`.
+const uci = cursor();
+
+const methods = {
+       get_sample1: {
+               call: function() {
+                       const num_cats = uci.get('example', 'animals', 'num_cats');
+                       const num_dogs = uci.get('example', 'animals', 'num_dogs');
+                       const num_parakeets = uci.get('example', 'animals', 'num_parakeets');
+                       const result = {
+                               num_cats,
+                               num_dogs,
+                               num_parakeets,
+                               is_this_real: false,
+                               not_found: null,
+                       };
+
+                       uci.unload();
+                       return result;
+               }
+       },
+
+       get_sample2: {
+               call: function() {
+                       const result = {
+                               option_one: {
+                                       name: "Some string value",
+                                       value: "A value string",
+                                       parakeets: ["one", "two", "three"],
+                               },
+                               option_two: {
+                                       name: "Another string value",
+                                       value: "And another value",
+                                       parakeets: [3, 4, 5],
+                               },
+                       };
+                       return result;
+               }
+       }
+};
+
+return { 'luci.example': methods };
index 6d3b67e820ee876804fb7642b61891f8bbe599ea..0c53772fdf107f524c23890d79c6eb4eaf3c800e 100644 (file)
@@ -9,6 +9,8 @@
 │               └── example
 │                   ├── form.js
 │                   ├── htmlview.js
+│                   ├── rpc-jsonmap-tablesection.js
+│                   ├── rpc-jsonmap-typedsection.js
 │                   └── rpc.js
 ├── Makefile
 ├── po
     │   └── uci-defaults
     │       └── 80_example
     └── usr
-        ├── libexec
-        │   └── rpcd
-        │       └── luci.example
         └── share
             ├── luci
             │   └── menu.d
             │       └── luci-app-example.json
             └── rpcd
-                └── acl.d
-                    └── luci-app-example.json
+                ├── acl.d
+                │   └── luci-app-example.json
+                └── ucode
+                    └── example.uc
 
 ```
 
@@ -68,9 +69,7 @@ LuCI apps do not have to have any additional files such as Lua scripts or UCI de
 
 ### Installing additional files
 
-Any additional files needed by this application should be placed in `root/` using the directory tree that applies. This example application needs a RPCd script to be installed, so it places a file in `root/usr/libexec/rpcd/` and calls it `luci.example`. Scripts must have their execution bit set, and committed to the git repository with the bit set.
-
-This example application also installs a file in `/etc/` by putting it in `root/etc/luci.example.yaml`.
+Any additional files needed by this application should be placed in `root/` using the directory tree that applies. This example application needs a ucode RPCd script to be installed, so it places a file in `root/usr/share/rpcd/ucode` and called `example.uc`.
 
 The OpenWrt packaging system will install these files automatically.